//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
/** @author  John A. Miller
 *  @author  Michael E. Cotterell
 *  @see     LICENSE (MIT style license file).
 */

package simopt

import scalation.process._

import scalation.linalgebra.{ MatrixD, VectorD }
import scalation.linalgebra_gen.Vectors.VectorI
import scalation.random.{ Random, Uniform, Variate, Exponential }
import scalation.optima.IntegerTabuSearch
import scalation.util.Monitor

/**
 * Emergency Room Model
 *
 * @param x       the staffing level
 * @param animate animate the model
 */
case class ERModel (x: VectorI, animate: Boolean = false) 
    extends Model ("UCFModel", animate) with ERParams 
{

    val aMeans = lambda.recipricol // arrival means
    val sMeans = mu.recipricol     // service time means

    // exponentially distributed arrival times
    val (nArrivalsFrontDoor, iArrivalFRV) = (Int.MaxValue, Exponential(aMeans(0)))
    val (nArrivalsAmbulance, iArrivalARV) = (Int.MaxValue, Exponential(aMeans(1)))

    // exponentially distributed service times
    val μTN = Array(sMeans(0)(0)), Exponential(sMeans(1)(0)))
    val μRN = Array(sMeans(0)(1)), Exponential(sMeans(1)(1)))
    val μMD = Array(sMeans(0)(2)), Exponential(sMeans(1)(2)))
    val μNP = Array(sMeans(0)(3)), Exponential(sMeans(1)(3)))
    val μAC = Array(sMeans(0)(4)), Exponential(sMeans(1)(4)))

    // turn of monitoring
    Monitor.traceOff

    // set the staffing level
    val nTN = x(0) // Triage Nurses
    val nRN = x(1) // Registered Nurses
    val nMD = x(2) // Medical Doctors
    val nNP = x(3) // Nurse Practitioners
    val nAC = x(4) // Administrative Clerks

    // sources
    val frontDoor     = new Source ("frontDoor", this, WalkInPatient, nArrivalsFrontDoor, iArrivalFRV, (10., 180.))
    val ambulance     = new Source ("ambulance", this, AmbulancePatient, nArrivalsAmbulance, iArrivalARV, (10., 85.))
 
    // queues
    val tnQ           = new WaitQueue ("tnQ", (120., 185.))
    val rnQ           = new WaitQueue ("rnQ", (300., 185.))
    val mdQ           = new WaitQueue ("mdQ", (400.,  85.))
    val npQ           = new WaitQueue ("npQ", (400., 285.))
    val acQ           = new WaitQueue ("acQ", (500., 185.))

    // resources
    val tn            = new Resource ("tn", tnQ, nTN, μTN(0), (170., 180.))
    val rn            = new Resource ("rn", rnQ, nRN, μRN(0), (350., 180.))
    val md            = new Resource ("md", mdQ, nMD, μMD(0), (450.,  80.))
    val np            = new Resource ("np", npQ, nNP, μNP(0), (450., 280.))
    val ac            = new Resource ("ac", acQ, nAC, μAC(0), (550., 180.))

    // sinks
    val door          = new Sink ("door", (650., 180.))

    // transports
    val frontDoor2tnQ = new Transport (   "2tnQ", Δ, frontDoor, tnQ )
    val ambulance2rnQ = new Transport (   "2rnQ", Δ, ambulance, rnQ )
    val tn2rnQ        = new Transport ( "tn2rnQ", Δ,        tn, rnQ )
    val rn2mdQ        = new Transport ( "rn2mdQ", Δ,        rn, mdQ )
    val rn2npQ        = new Transport ( "rn2npQ", Δ,        rn, npQ )
    val md2acQ        = new Transport ( "md2acQ", Δ,        md, acQ )
    val np2acQ        = new Transport ( "np2acQ", Δ,        np, acQ )
    val ac2door       = new Transport ("ac2door", Δ,        ac, door)

    // random number generator
    val r = Random () 
    
    // add components to the simulation model
    addComponents (List (
        frontDoor, 
        ambulance, 
        tnQ, tn, 
        rnQ, rn, 
        mdQ, md, 
        npQ, np, 
        acQ, ac, 
        door,
        frontDoor2tnQ, ambulance2rnQ,
        tn2rnQ, rn2mdQ, rn2npQ, md2acQ, np2acQ, ac2door
    )) // addComponents 

    // convenience function to get sum of mean waiting times
    def sumMeanWaitingTimes () = Seq(tnQ, rnQ, mdQ, npQ, acQ).map(r => r.durationStat.mean).reduceLeft(_+_)
    f = sumMeanWaitingTimes

    var high = 0 // count the number of high priority patients
    var low  = 0 // count the number of low priority patients

    // actor for patients
    abstract class Patient (pType: String) extends SimActor ("p-%s".format(pType), this)
    {
        // assign patient class
        val pclass = if (r.gen <= 0.25) 0 else 1
    } // Patient

    // actor and script for patients who arrive by ambulance
    case class AmbulancePatient () extends Patient ("ambulance")
    {
        def act () 
        {
            ambulance2rnQ.move ()
            if (rn.busy) rnQ.waitIn else rnQ.noWait
            rn.utilize(μRN(pclass).gen)
            rn.release
            pclass match {
                case 0 => {
                    low += 1
                    rn2mdQ.move
                    if (md.busy) mdQ.waitIn else mdQ.noWait
                    md.utilize(μMD(pclass).gen)
                    md.release
                    md2acQ.move
                }
                case 1 => {
                    high += 1
                    rn2npQ.move
                    if (np.busy) npQ.waitIn else npQ.noWait
                    np.utilize(μNP(pclass).gen)
                    np.release
                    np2acQ.move
                }
            } // match
            if (ac.busy) acQ.waitIn else acQ.noWait
            ac.utilize(μAC(pclass).gen)
            ac.release
            ac2door.move
            door.leave
        } // act
    } // AmbulancePatient
    
    // actor and script for patients who arrive through the front door
    case class WalkInPatient () extends Patient ("walkin")
    {
        def act () 
        {
            frontDoor2tnQ.move
            if (tn.busy) tnQ.waitIn else tnQ.noWait
            tn.utilize(μTN(pclass).gen)
            tn.release
            tn2rnQ.move
            if (rn.busy) rnQ.waitIn else rnQ.noWait
            rn.utilize(μRN(pclass).gen)
            rn.release
            pclass match {
                case 0 => {
                    low += 1
                    rn2mdQ.move
                    if (md.busy) mdQ.waitIn else mdQ.noWait
                    md.utilize(μMD(pclass).gen)
                    md.release
                    md2acQ.move
                }
                case 1 => {
                    high += 1
                    rn2npQ.move
                    if (np.busy) npQ.waitIn else npQ.noWait
                    np.utilize(μNP(pclass).gen)
                    np.release
                    np2acQ.move
                }
            } // match
            if (ac.busy) acQ.waitIn else acQ.noWait
            ac.utilize(μAC(pclass).gen)
            ac.release
            ac2door.move
            door.leave
        } // act
    } // WalkInPatient

} // ERModel

object ERModelTest extends App
{
    val x = new VectorI(1, 1, 1, 1, 1)
    val m = new EmergencyModel(x, false)
    val results = m.simulate()
} // ERModelTest 

trait EROpt extends ERParams 
{

    val d = 1.0
    val s = VectorD (10, 50, 120, 80, 8)
    val f = VectorD (80, 150)

    // Utility Function for Healthcare Application 
    // Uses the Process-Interaction Model
    def u_p (x: VectorI): Double = {
        for (i <- 0 until x.dim if x(i) < 1)  return Double.NegativeInfinity
        for (i <- 0 until x.dim if x(i) > 10) return Double.NegativeInfinity
        _u(x)
    } // f

    // Utility Function for Healthcare Application 
    // Uses the Process-Interaction Model
    // Note: This version is used with Multiple Linear Regression
    def _u_p (x: VectorI): Double = {
        val m = new ERModel(x)              // create model 
        val results = m.simulate()          // simulate and gather stats
        val w = m.sumMeanWaitingTimes()     // sum of average waiting times
        val n = VectorD (m.low, m.high)     // patients served of each type
        val c = x dot s                     // operating payroll cost
        val r = f dot n                     // revenue for patient service
        val p = r - c                       // net profit
        p - d * w * n.mean                  // return overall utility
    } // u_p

} // EmergencyOpt

object EROptTest extends App with EROpt 
{
    val x0          = VectorI (1, 1, 1, 1, 1)             // starting point
    val its         = new IntegerTabuSearch (u_p)         // setup optimizer
    val (x, result) = its.maximize (x0)                   // get results
    println ("x = %s; u_p value = %f".format(x, result))  // print results
} // EROptTest
